package de.mmis.core.pojop.server;

import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;

import org.apache.log4j.Logger;

import de.mmis.core.base.abstracttree.TreeParserException;
import de.mmis.core.pojop.processor.RequestProcessorFactory;
import de.mmis.core.pojop.processor.RequestProcessorFactory.RequestProcessor;
import de.mmis.core.pojop.request.AbstractRequest;
import de.mmis.core.sexpression.SExpressionParser;

/**
 * implementation of the server interface providing a telnet based communication
 * using S-Expressions intended to be used manually by telnet clients
 * 
 * @author Christian Koch
 */
public class TelnetServer extends SocketServer {

	private final static Logger LOGGER = Logger.getLogger(TelnetServer.class);

	public TelnetServer(int port,
			RequestProcessorFactory<AbstractRequest> requestProcessorFactory)
			throws IOException {
		super(port, requestProcessorFactory);

		this.telnetHistory = new TelnetHistory(1000);
	}

	@Override
	public de.mmis.core.pojop.server.AbstractServer.SessionHandler createSessionHandler(
			Socket socket) {
		return new TelnetSessionHandler(socket,
				this.requestProcessorFactory.createRequestProcessor());
	}

	TelnetHistory telnetHistory;

	/**
	 * class storing previously typed in telnet commands, so they can be reused
	 * 
	 * @author Christian Koch
	 */
	private class TelnetHistory {
		private ArrayList<byte[]> history;
		int size;
		int curpos;
		int anzelems;

		public TelnetHistory(int size) {
			this.history = new ArrayList<byte[]>(size);
			for (int i = 0; i < size; i++)
				this.history.add(null);
			this.size = size;
			this.curpos = 0;
			this.anzelems = 0;
		}

		public synchronized void addToHistory(byte[] line) {
			this.history.set(this.curpos, line);
			this.curpos = (this.curpos + 1) % this.size;
			if (this.anzelems < this.size)
				this.anzelems++;
		}

		public synchronized byte[] getHistoryEntry(int pos) {
			if (pos >= 0 && pos < this.size)
				return this.history.get(pos);
			return null;
		}
	}

	/**
	 * a session handler to communicate with the given client socket. all
	 * communication specification is done in this class
	 * 
	 * @author Christian Koch
	 */
	private class TelnetSessionHandler extends SocketSessionHandler {
		private static final String PROMPT = "> ";

		private int width = 0, height = 0;
		private int curpos = 0;
		private ArrayList<Byte> curline = new ArrayList<Byte>();

		/**
		 * creates a new session handler for socket connections
		 * 
		 * @param socket
		 *            client socket
		 */
		public TelnetSessionHandler(Socket socket,
				RequestProcessor<AbstractRequest> requestProcessor) {
			super(socket, requestProcessor);
		}

		/**
		 * synchronized, writes given binary data to socket output stream
		 * 
		 * @param data
		 *            data to be written
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		@Override
		public void outwrite(byte[] data) throws IOException {
			synchronized (this.outstream) {
				for (int i = 0; i < data.length; i++)
					outwrite(data[i]);
			}
		}

		/**
		 * synchronized, writes given byte to socket output stream
		 * 
		 * @param data
		 *            byte to be written
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		@Override
		public void outwrite(int data) throws IOException {
			synchronized (this.outstream) {
				if (data == 10)
					this.outstream.write(13);
				this.outstream.write(data);
				if (data == 255)
					this.outstream.write(data);
			}
		}

		@Override
		public void init() throws IOException {

			super.init();

			try {
				byte[] options = {
						// tells client not to echo locally
						(byte) 255, // IAC
						(byte) 251, // WILL
						(byte) 1, // ECHO

						// tells client to do character mode
						(byte) 255, // IAC
						(byte) 251, // WILL
						(byte) 3, // SUPPRESS-GO-AHEAD

						// tells client to do NAWS (send client window size)
						(byte) 255, // IAC
						(byte) 253, // DO
						(byte) 31, // NAWS

						// tells client not to do NAWS (refuse further window
						// size change)
						(byte) 255, // IAC
						(byte) 254, // DON'T
						(byte) 31, // NAWS
				};

				this.outstream.write(options);
				this.outstream.flush();

				// expect following answer from client (needed for user-friendly
				// options. if failing user should use socket server instead)
				if (this.instream.read() != 255)
					this.keepAnswering = false; // IAC
				else if (this.instream.read() != 253)
					this.keepAnswering = false; // DO
				else if (this.instream.read() != 1)
					this.keepAnswering = false; // ECHO

				else if (this.instream.read() != 255)
					this.keepAnswering = false; // IAC
				else if (this.instream.read() != 253)
					this.keepAnswering = false; // DO
				else if (this.instream.read() != 3)
					this.keepAnswering = false; // SUPRESS-GO-AHEAD

				else if (this.instream.read() != 255)
					this.keepAnswering = false; // IAC
				else if (this.instream.read() != 251)
					this.keepAnswering = false; // WILL
				else if (this.instream.read() != 31)
					this.keepAnswering = false; // NAWS

				else if (this.instream.read() != 255)
					this.keepAnswering = false; // IAC
				else if (this.instream.read() != 250)
					this.keepAnswering = false; // SB
				else if (this.instream.read() != 31)
					this.keepAnswering = false; // NAWS
				else {
					this.width = this.instream.read() * 256;
					this.width += this.instream.read();
					this.height = this.instream.read() * 256;
					this.height += this.instream.read();
					if (this.instream.read() != 255)
						this.keepAnswering = false; // IAC
					else if (this.instream.read() != 240)
						this.keepAnswering = false; // SE

					else if (this.instream.read() != 255)
						this.keepAnswering = false; // IAC
					else if (this.instream.read() != 252)
						this.keepAnswering = false; // WON'T
					else if (this.instream.read() != 31)
						this.keepAnswering = false; // NAWS
				}

				if (this.keepAnswering)
					stdout("Start telnet communication with "
							+ this.socket.getInetAddress() + ":"
							+ this.socket.getPort());
				else {
					stdout("connected client ("
							+ this.socket.getInetAddress()
							+ ":"
							+ this.socket.getPort()
							+ ") did not answer as expected, no valid telnet client, closing connection");
					outwrite("\n\nyou are not using a valid telnet client. try to connect to raw socket server instead\n\n");
					outflush();
				}

				if (this.keepAnswering) {
					outwrite("Type \":help\" for a list of available commands");
					newLine();
					outwrite("Type \":quit\" to stop communication");
					newLine();
					outwrite("Type \":observe\" to observe object (if it is observable)");
					newLine();

					LOGGER.debug("Window size: " + this.width + "x"
							+ this.height);
				}
			} catch (IOException e) {
				LOGGER.error("IO error in client socket", e);
				this.keepAnswering = false;
			}
		}

		boolean newline = false; // used to consume newline character "\n" after
									// "\r"

		@Override
		public AbstractRequest nextRequest() {
			try {
				outwrite(PROMPT);
				outflush();

				int c = this.instream.read();
				this.curline.clear();
				this.curpos = 0;
				int curHistoryPosition = -1;

				// waiting for client requests
				while (this.keepAnswering && c != -1) {
					// System.out.println(c);

					if ((c == 0 || c == 10) && this.newline) {
						this.newline = false;
						c = this.instream.read();
						continue;
					}
					this.newline = false;
					if (c == 13) {
						newLine();
						this.newline = true;

						return nextRequestLine();
					} else if (c == 27) {
						c = this.instream.read();
						if (c == 91) {
							c = this.instream.read();
							if (c == 68) // cursor left
							{
								// System.out.println("cursor left");
								cursorLeft();
							} else if (c == 65) // cursor up
							{
								// System.out.println("cursor up");

								if (curHistoryPosition == -1)
									if (TelnetServer.this.telnetHistory.anzelems > 0)
										curHistoryPosition = TelnetServer.this.telnetHistory.curpos;
								if (curHistoryPosition != -1
										&& (TelnetServer.this.telnetHistory.curpos
												- curHistoryPosition + TelnetServer.this.telnetHistory.size)
												% TelnetServer.this.telnetHistory.size < TelnetServer.this.telnetHistory.anzelems)
									curHistoryPosition = (curHistoryPosition
											+ TelnetServer.this.telnetHistory.size - 1)
											% TelnetServer.this.telnetHistory.size;

								setToHistory(curHistoryPosition);

								// cursorUp();
							} else if (c == 66) // cursor down
							{
								// System.out.println("cursor down");

								if (curHistoryPosition != -1) {
									curHistoryPosition = (curHistoryPosition + 1)
											% TelnetServer.this.telnetHistory.size;
									if (curHistoryPosition == TelnetServer.this.telnetHistory.curpos)
										curHistoryPosition = -1;
									setToHistory(curHistoryPosition);
								}

								// cursorDown();
							} else if (c == 67) // cursor right
							{
								// System.out.println("cursor right");
								cursorRight();
							} else
								continue;
						} else if (c == 51) // delete
						{
							c = this.instream.read();
							if (c == 126) {
								// System.out.println("delete");
								if (this.curpos < this.curline.size()) {
									removeByte();
								}
							} else
								continue;
						} else
							continue;
					} else if (c == 127) // backspace
					{
						// System.out.println("backspace");
						if (this.curpos > 0) {
							cursorLeft();
							removeByte();
						}
					} else if (c == 255) // special command byte, escaped by
											// doubling
					{
						c = this.instream.read();
						if (c == 255)
							addByte(c);
						else
							continue;
					} else {
						addByte(c);
					}
					outflush();
					c = this.instream.read();
				}
				outwrite("bye bye\n");

			} catch (IOException e) {
				LOGGER.error("IO error in client socket", e);
			}
			return null;
		}

		/**
		 * recall command from history
		 * 
		 * @param curHistoryPosition
		 *            position in history
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void setToHistory(int curHistoryPosition) throws IOException {
			revertToPosition(0);
			for (int i = 0; i < this.curline.size(); i++)
				writeByte(32);
			revertToPosition(0);
			this.curline.clear();

			if (curHistoryPosition != -1) {
				byte[] data = TelnetServer.this.telnetHistory
						.getHistoryEntry(curHistoryPosition);
				for (int i = 0; i < data.length; i++)
					addByte(data[i]);
			}
		}

		/**
		 * adds byte into current position
		 * 
		 * @param c
		 *            byte to add
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void addByte(int c) throws IOException {
			this.curline.add(this.curpos, new Byte((byte) c));

			writeByte(c);

			int temppos = this.curpos;

			while (this.curpos < this.curline.size())
				writeByte(this.curline.get(this.curpos).intValue());

			revertToPosition(temppos);
		}

		/**
		 * removes byte at current position
		 * 
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void removeByte() throws IOException {
			this.curline.remove(this.curpos);

			int temppos = this.curpos;

			while (this.curpos < this.curline.size())
				writeByte(this.curline.get(this.curpos).intValue());
			writeByte(32); // white

			revertToPosition(temppos);
		}

		/**
		 * move cursor to specified position in current command string
		 * 
		 * @param temppos
		 *            position to move to
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void revertToPosition(int temppos) throws IOException {
			if (temppos < this.curpos) {
				while (this.curpos - temppos >= this.width)
					cursorUp();
				while (this.curpos > temppos)
					cursorLeft();
			} else {
				while (temppos - this.curpos >= this.width)
					cursorDown();
				while (this.curpos < temppos)
					cursorRight();
			}
		}

		/**
		 * write given byte to stream
		 * 
		 * @param c
		 *            byte to write to stream
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void writeByte(int c) throws IOException {
			outwrite(c);
			this.curpos++;
			if ((this.curpos + PROMPT.length()) % this.width == 0)
				newLine();
		}

		/**
		 * simulate "cursor down" command
		 * 
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void cursorDown() throws IOException {
			if (this.curpos <= this.curline.size() - this.width) {
				outwrite(27);
				outwrite(91);
				outwrite(66);
				this.curpos += this.width;
			}
		}

		/**
		 * simulate "cursor up" command
		 * 
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void cursorUp() throws IOException {
			if (this.curpos >= this.width) {
				outwrite(27);
				outwrite(91);
				outwrite(65);
				this.curpos -= this.width;
			}
		}

		/**
		 * simulate "cursor right" command
		 * 
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void cursorRight() throws IOException {
			if (this.curpos < this.curline.size()) {
				this.curpos++;
				if ((this.curpos + PROMPT.length()) % this.width == 0)
					newLine();
				else {
					outwrite(27);
					outwrite(91);
					outwrite(67);
				}
			}
		}

		/**
		 * simulate "cursor left" command
		 * 
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void cursorLeft() throws IOException {
			if (this.curpos > 0) {
				if ((this.curpos + PROMPT.length()) % this.width == 0) {
					outwrite(27);
					outwrite(91);
					outwrite(65);
					for (int i = 1; i < this.width; i++) {
						outwrite(27);
						outwrite(91);
						outwrite(67);
					}
				} else {
					outwrite(27);
					outwrite(91);
					outwrite(68);
				}
				this.curpos--;
			}
		}

		/**
		 * write new line to stream
		 * 
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private void newLine() throws IOException {
			outwrite("\n");
		}

		/**
		 * answer typed in command and write result to output stream
		 * 
		 * @param curline
		 *            typed in command
		 * @throws IOException
		 *             an error occurred while writing to stream
		 */
		private AbstractRequest nextRequestLine() throws IOException {
			byte[] data = new byte[this.curline.size()];
			for (int i = 0; i < data.length; i++)
				data[i] = this.curline.get(i).byteValue();

			TelnetServer.this.telnetHistory.addToHistory(data);

			try {
				return nextRequest(SExpressionParser.parse(data));
			} catch (TreeParserException e) {
				return new AbstractRequest(e);
			}
		}

		@Override
		public String getConnectionTypeName() {
			return "Telnet";
		}
	}
}
